#!/usr/bin/env python3
# Copyright (C) 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Like llvm-symbolizer for UI JS/TS sources.

This script is used to "symbolize" UI crashes. It takes a crash, typically
copied from a bug reports, of the form:

(https://ui.perfetto.dev/v12.1.269/frontend_bundle.js:7639:61) at foo()
(https://ui.perfetto.dev/v12.1.269/frontend_bundle.js:9235:29) at bar()

it fetches the corresponding source maps and emits in output a translated
crash report, of the form:

https://android.googlesource.com/platform/external/perfetto/+/de4db33f/ui/src/foo.ts#61 at foo()
https://android.googlesource.com/platform/external/perfetto/+/de4db33f/ui/src/baz.ts#300 at bar()
"""

import logging
import re
import sys
import tempfile
import urllib.request
import ssl
import os

try:
  import sourcemap
except:
  print('Run `pip3 install sourcemap` and try again')
  sys.exit(1)


GERRIT_BASE_URL = 'https://android.googlesource.com/platform/external/perfetto/'

def fetch_url_cached(url):
  normalized = re.sub('[^a-zA-Z0-9-._]', '_', url)
  local_file = os.path.join(tempfile.gettempdir(), normalized)
  if os.path.exists(local_file):
    logging.debug('Using %s', local_file)
    with open(local_file, 'r') as f:
      return f.read()
  context = ssl._create_unverified_context()
  logging.info('Fetching %s', url)
  resp = urllib.request.urlopen(url, context=context)
  contents = resp.read().decode()
  with open(local_file, 'w') as f:
    f.write(contents)
  return contents


def Main():
  if len(sys.argv) > 1:
    with open(sys.argv[1], 'r') as f:
      txt = f.read()
  else:
    if sys.stdin.isatty():
      print('Paste the crash log and press CTRL-D\n')
    txt = sys.stdin.read()

  # Look for the GIT commitish appended in crash reports. This is not required
  # for resolving the sourcemaps but helps generating better links.
  matches = re.findall(r'([a-f0-9]{40})\sUA', txt)
  git_rev = matches[0] if matches else 'HEAD'

  matches = re.findall(r'((\bhttp.+?\.js):(\d+):(\d+))', txt)
  maps_by_url = {}
  sym_lines = ''
  for entry in matches:
    whole_token, script_url, line, col = entry
    map_url = script_url + '.map'
    if map_url in maps_by_url:
      srcmap = maps_by_url[map_url]
    else:
      map_file_contents = fetch_url_cached(map_url)
      srcmap = sourcemap.loads(map_file_contents)
      maps_by_url[map_url] = srcmap
    sym = srcmap.lookup(int(line), int(col))
    src = sym.src.replace('../../', '')
    sym_url = '%s#%s' % (src, sym.src_line)
    if src.startswith('../out/ui/'):
      src = src.replace('../out/ui/', 'ui/')
      sym_url = GERRIT_BASE_URL + '/+/%s/%s#%d' % (git_rev, src, sym.src_line)
    sym_lines += sym_url + '\n'
    txt = txt.replace(whole_token, sym_url)

  print(txt)
  print('\nResolved symbols:\n' + sym_lines)


if __name__ == '__main__':
  logging.basicConfig(level=logging.INFO)
  sys.exit(Main())
